Register File

Magma has an experimental feature introducing a register file primitive.

To use this, first install the magma branch:

git clone https://github.com/phanrahan/magma
cd magma
git checkout regfile-primitive
pip install -e .

The RegisterFile primitive is a Generator that takes in arguments height, data_width. This will create height registers each storing a Bits[data_width].

To read from a register, you can use the __getitem__ syntax, such as reg_file[addr] where addr is a magma value of type Bits[clog2(height)]. This will add a read port to the generated register file.

Simililarly, to write to a register, you can use the __setitem__ syntax, such as reg_file[addr] = data where addr is a magma value of type Bits[clog2(height)] and data is a magma value of type Bits[data_width]. This will add a write port to the generated register file. NOTE that this uses = (assignment) instead of @= which is normally used for wiring.

The RegisterFile uses last connect (write) semantics. If two statements write to the register file, and their dynamic address values match, the value written by the last executed statement will take priority.

The RegisterFile forwards writes within the same cycle (combinational writes), so if a read statement reads from the same dynamic address as a write statement, the read value will equal the write value.

Planned features (feedback welcome):

  • Support imatmul (@=) syntax to be consistent with wire
  • Optional write forwarding (writes have a one cycle delay)
  • Integration with sequential syntax
  • Convenient syntax for enable logic (when)
  • Generate verilog always blocks

Here's a basic example and test


In [1]:
import magma as m
from magma.primitives.register_file import RegisterFile

height = 4
data_width = 4
addr_width = m.bitutils.clog2(height)

class Main(m.Circuit):
    io = m.IO(
        write_addr=m.In(m.Bits[addr_width]),
        write_data=m.In(m.Bits[data_width]),
        read_addr=m.In(m.Bits[addr_width]),
        read_data=m.Out(m.Bits[data_width])
    ) + m.ClockIO(has_async_reset=True)
    
    reg_file = RegisterFile(height, data_width)
    reg_file[io.write_addr] = io.write_data
    io.read_data @= reg_file[io.read_addr]

m.compile("build/test_register_file_primitive_basic", Main, inline=True)


WARNING:magma:'IO = [...]' syntax is deprecated, use 'io = IO(...)' syntax instead
WARNING:magma:'IO = [...]' syntax is deprecated, use 'io = IO(...)' syntax instead
WARNING:magma:'IO = [...]' syntax is deprecated, use 'io = IO(...)' syntax instead
WARNING:magma:'IO = [...]' syntax is deprecated, use 'io = IO(...)' syntax instead
WARNING:magma:'IO = [...]' syntax is deprecated, use 'io = IO(...)' syntax instead
WARNING:magma:'IO = [...]' syntax is deprecated, use 'io = IO(...)' syntax instead
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-1-b367022aec7e> in <module>
      1 import magma as m
----> 2 from magma.primitives.register_file import RegisterFile
      3 
      4 height = 4
      5 data_width = 4

ModuleNotFoundError: No module named 'magma.primitives.register_file'; 'magma.primitives' is not a package

In [ ]:
import fault
import tempfile
tester = fault.Tester(Main, Main.CLK)
tester.circuit.CLK = 0
for i in range(4):
    tester.circuit.write_addr = i
    tester.circuit.write_data = i
    tester.step(2)
for i in range(4):
    tester.circuit.read_addr = i
    tester.eval()
    tester.circuit.read_data.expect(i)
    
# Test combinational write
tester.circuit.read_addr = 1
tester.eval()
tester.circuit.read_data.expect(1)
tester.circuit.write_addr = 1
tester.circuit.write_data = 2
tester.eval()
tester.circuit.read_data.expect(2)


with tempfile.TemporaryDirectory() as dir_:
    tester.compile_and_run("verilator", directory=dir_, flags=['-Wno-unused'])

Here's an example and test that demonstrates the "last connect/write" semantics


In [ ]:
import magma as m
from magma.primitives.register_file import RegisterFile

height = 4
data_width = 4
addr_width = m.bitutils.clog2(height)

class Main2(m.Circuit):
    io = m.IO(
        write_addr0=m.In(m.Bits[addr_width]),
        write_data0=m.In(m.Bits[data_width]),
        write_addr1=m.In(m.Bits[addr_width]),
        write_data1=m.In(m.Bits[data_width]),
        read_addr0=m.In(m.Bits[addr_width]),
        read_data0=m.Out(m.Bits[data_width]),
        read_addr1=m.In(m.Bits[addr_width]),
        read_data1=m.Out(m.Bits[data_width])
    ) + m.ClockIO(has_async_reset=True)
    reg_file = RegisterFile(height, data_width)
    reg_file[io.write_addr0] = io.write_data0
    io.read_data0 @= reg_file[io.read_addr0]
    reg_file[io.write_addr1] = io.write_data1
    io.read_data1 @= reg_file[io.read_addr1]

m.compile("build/test_register_file_primitive_two", Main2, inline=True)

import fault
import tempfile
tester = fault.Tester(Main2, Main2.CLK)
tester.circuit.CLK = 0
for i in range(4):
    tester.circuit.write_addr0 = i
    tester.circuit.write_data0 = 3 - i
    tester.circuit.write_addr1 = 3 - i
    tester.circuit.write_data1 = i
    tester.step(2)
for i in range(4):
    tester.circuit.read_addr0 = i
    tester.circuit.read_addr1 = 3 - i
    tester.eval()
    tester.circuit.read_data0.expect(3 - i)
    tester.circuit.read_data1.expect(i)

# Test priority
tester.circuit.write_addr0 = 3
tester.circuit.write_data0 = 3
tester.circuit.write_addr1 = 3
tester.circuit.write_data1 = 4
tester.step(2)
tester.circuit.read_addr0 = 3
tester.eval()
tester.circuit.read_data0.expect(4)
with tempfile.TemporaryDirectory() as dir_:
    tester.compile_and_run("verilator", directory=dir_, flags=['-Wno-unused'])

In [ ]: